/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package skyproc;

import java.io.IOException;
import java.util.*;
import java.util.zip.DataFormatException;
import lev.*;
import skyproc.exceptions.BadParameter;
import skyproc.exceptions.BadRecord;

/**
 * A specialized collection of subrecords. Think of it as a special SkyProc
 * ArrayList used for subrecords.
 *
 * @param <T> The type of subrecord the group contains.
 * @author Justin Swanson
 */
class SubList<T extends SubRecord> extends SubRecord implements Iterable<T> {

    ArrayList<T> collection = new ArrayList<>();
    T prototype;
    Type counterType = Type.NULL;
    boolean allowDups = true;
    private int counterLength = 4;

    SubList(T prototype_) {
        super(prototype_.type);
        prototype = prototype_;
    }

    SubList(Type counter, int counterLength, T prototype_) {
        this(prototype_);
        counterType = counter;
        this.counterLength = counterLength;
        type = new Type[prototype.type.length + 1];
        System.arraycopy(prototype.type, 0, type, 0, prototype.type.length);
        type[prototype_.type.length] = counterType;
    }

    @Override
    int getHeaderLength() {
        return 0;
    }

    @Override
    Boolean isValid() {
        return !collection.isEmpty();
    }

    /**
     *
     * @param s Record to check for containment.
     * @return True if an equal() record exists within the SubRecordList.
     */
    public boolean contains(T s) {
        return collection.contains(s);
    }

    /**
     *
     * @param t Record to obtain the index of.
     * @return Index number that the first element matching the input contains,
     * or -1 if no element exists in the collection.
     */
    public int indexOf(T t) {
        return collection.indexOf(t);
    }

    /**
     *
     * @param i Index of the item to retrieve.
     * @return The ith item.
     */
    public T get(int i) {
        return collection.get(i);
    }

    /**
     * Adds an item to the list. Some groups allow duplicate items, some do not,
     * depending on the internal specifications and context. This function
     * returns true if the item was successfully added to list, or false if an
     * equal one already existed, and the group did not allow duplicates.
     *
     * @param item Item to add to the list.
     * @return true if the item was added to the list. False if an equal item
     * already existed in the list, and duplicates were not allowed.
     */
    public boolean add(T item) {
        if (allow(item)) {
            collection.add(item);
            return true;
        } else {
            return false;
        }
    }

    boolean allow(T item) {
        return allowDups || !collection.contains(item);
    }

    public boolean addAtIndex(T item, int i) {
        if (allow(item)) {
            collection.add(i, item);
            return true;
        } else {
            return false;
        }
    }

    void allowDuplicates(boolean on) {
        allowDups = on;
    }

    /**
     *
     * @return True if the SubRecordList allows duplicate records.
     */
    public boolean allowsDuplicates() {
        return allowDups;
    }

    /**
     *
     * @param item Item to remove from the list.
     * @return True if an item was removed.
     */
    public boolean remove(T item) {
        if (collection.contains(item)) {
            collection.remove(item);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Removes an item based on its index in the list.
     *
     * @param i Index of the item to remove.
     */
    public void remove(int i) {
        collection.remove(i);
    }

    /**
     *
     * @return The number of items currently in the list.
     */
    public int size() {
        return collection.size();
    }

    public void clear() {
        collection.clear();
    }

    /**
     *
     * @return True if list is empty, and size == 0.
     */
    public boolean isEmpty() {
        return collection.isEmpty();
    }

    /**
     * This function will replace all records in the SubRecordList with the ones
     * given.<br><br> WARNING: All existing records will be lost.
     *
     * @param in ArrayList of records to replace the current ones.
     */
    public void setRecordsTo(ArrayList<T> in) {
        collection = in;
    }

    /**
     * This function will add all records given to the list using add().
     *
     * @param in ArrayList of records to add in.
     */
    public void addRecordsTo(ArrayList<T> in) {
        for (T t : collection) {
            collection.add(t);
        }
    }

    /*
     * SkyBash methods.
     */
    /**
     * Merges SubLists with logging capabilities. Does not check to see if list
     * objects are the same.
     *
     * @param no The new SubList to be merged.
     * @param bo The base SubList, to prevent base data from being re-merged.
     * @return The modified SubList.
     */
    @Override
    public SubRecord merge(SubRecord no, SubRecord bo) {
        SubList<T> o = this;
        if (!(no == null && bo == null && (no instanceof SubList) && (bo instanceof SubList))) {
            final SubList<T> newlist = (SubList<T>) no;
            final SubList<T> baselist = (SubList<T>) bo;
            SkyBash.merge(o.collection, newlist.collection, baselist.collection, type[0]);
        }
        return o;
    }

    /**
     * Replaces entries in a SubList, with logging capabilities.
     *
     * @param tempListNew The new ArrayList to be replaced for the SubList.
     * @param tempListBase The base ArrayList, to prevent a base list from
     * replaced a new one.
     * @return The modified ArrayList.
     */
    public ArrayList<T> replace(ArrayList<T> tempListNew, ArrayList<T> tempListBase) {
        ArrayList<T> tempList = this.collection;
        if (!tempList.equals(tempListNew)) {
            if (!tempListBase.equals(tempListNew)) {
                tempList.clear();
                for (int i = 0; i < tempListNew.size(); i++) {
                    tempList.add(tempListNew.get(i));
                    if (SkyBash.fullLogging) {
                        SPGlobal.log(type[0].toString() + ": ", "Replaced list for " + type[0].toString() + " from "
                                + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod);
                    }
                }
            }
        }
        return tempList;
    }

    /**
     * Replaces entries in a SubList, with logging capabilities.
     *
     * @param tempListNew The new SubList to replace the old SubList.
     * @param tempListBase The base SubList, to prevent a base list from
     * replaced a new one.
     * @return The modified SubList.
     */
    SubList<T> replace(SubList<T> tempListNew, SubList<T> tempListBase) {
        SubList<T> tempList = this;
        if (!tempList.equals(tempListNew)) {
            if (!tempListBase.equals(tempListNew)) {
                tempList.clear();
                for (int i = 0; i < tempListNew.size(); i++) {
                    tempList.add(tempListNew.get(i));
                    if (SkyBash.fullLogging) {
                        SPGlobal.log(type[0].toString() + ": ", "Replaced list for " + type[0].toString() + " from "
                                + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod);
                    }
                }
            }
        }
        return tempList;
    }

    /**
     * A specialized merger for a list of SubFormInts. Used for perks to
     * preserve the initial integer.
     *
     * @param tempListNew The new SubList<SubFormInt> to replace the old
     * SubList.
     * @param tempListBase The base SubList<SubFormInt>, to prevent a base list
     * from replaced a new one.
     * @return The modified SubList<SubFormInt>.
     */
    SubList<SubFormInt> mergeListSFISpecial(SubList<SubFormInt> tempListNew, SubList<SubFormInt> tempListBase) {

        SubList<SubFormInt> tempList = (SubList<SubFormInt>) this;

        //Integer used to hold the number for synchronization.
        int tempnum = 0;

        SubFormIntStorage first = new SubFormIntStorage(tempList);
        SubFormIntStorage newlist = new SubFormIntStorage(tempListNew);
        SubFormIntStorage baselist = new SubFormIntStorage(tempListBase);

        //Just a method to grab any one of the initial numbers.
        for (int i = 0; i < first.size; i++) {
            try {
                tempnum = first.i.get(i);
                break;
            } catch (NullPointerException e) {
                tempnum = 0;
                break;
            }
        }

        for (int i = 0; i < baselist.size; i++) {
            FormID baseID = baselist.id.get(i);
            if (!newlist.id.contains(baseID) && first.id.contains(baseID)) {
                first.remove(baseID);
                if (SkyBash.fullLogging) {
                    SPGlobal.log(type[0].toString() + ": ", "Removed " + type[0].toString() + " " + baseID + " to "
                            + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod);
                }
            }
        }
        for (int i = 0; i < newlist.size; i++) {
            FormID newID = newlist.id.get(i);
            if (!first.id.contains(newID) && !baselist.id.contains(newID)) {
                first.add(new SubFormInt(counterType, newID, tempnum));
                if (SkyBash.fullLogging) {
                    SPGlobal.log(type[0].toString() + ": ", "Merged " + type[0].toString() + " " + newID + " to "
                            + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod);
                }
            }
        }

        tempList.clear();
        tempList.addRecordsTo(first.sfi);
        return tempList;
    }

    /**
     * A specialized merger for a list of SubFormInts. Used for items/factions
     * to update the integers associated when forms are matching..
     *
     * @param tempListNew The new SubList<SubFormInt> to replace the old
     * SubList.
     * @param tempListBase The base SubList<SubFormInt>, to prevent a base list
     * from replaced a new one.
     * @return The modified SubList<SubFormInt>.
     */
    SubList<SubFormInt> mergeList(SubList<SubFormInt> tempListNew, SubList<SubFormInt> tempListBase) {

        SubList<SubFormInt> tempList = (SubList<SubFormInt>) this;

        SubFormIntStorage first = new SubFormIntStorage(tempList);
        SubFormIntStorage newlist = new SubFormIntStorage(tempListNew);
        SubFormIntStorage baselist = new SubFormIntStorage(tempListBase);

        //Removes any entries that were removed from the original NPC.
        for (int i = 0; i < baselist.size; i++) {
            FormID baseID = baselist.id.get(i);
            if (!newlist.id.contains(baseID) && first.id.contains(baseID)) {
                first.remove(baseID);
                if (SkyBash.fullLogging) {
                    SPGlobal.log(type[0].toString() + ": ", "Removed " + type[0].toString() + " " + baseID + " to "
                            + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod);
                }
            }
        }

        for (int i = 0; i < newlist.size; i++) {
            FormID newID = newlist.id.get(i);
            int newValue = newlist.i.get(i);
            //Checks if the value exists, in which case it updates the count as per below.
            if (first.id.contains(newID)) {
                //If the base list contains it, and the new one is different, or if the base list doesn't contain it
                //  and the new one does, it updates the integer.
                if ((baselist.map.get(newID) != null && newValue != baselist.map.get(newID))
                        || first.map.get(newID).equals(newValue)) {
                    first.remove(newID);
                    first.add(new SubFormInt(this.counterType, newID, newValue));
                    if (SkyBash.fullLogging) {
                        SPGlobal.log(type[0].toString() + ": ", "Merged integer for + " + type[0].toString() + " for ID " + newID + " to "
                                + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod
                                + " to count " + newValue);
                    }
                }
                //If the base list doesn't contain it, and implicitly the value isn't on the first list,
                //  it adds the new value.
            } else if (!baselist.id.contains(newID)) {
                first.add(new SubFormInt(this.counterType, newID, newValue));
                if (SkyBash.fullLogging) {
                    SPGlobal.log(type[0].toString() + ": ", "Added " + type[0].toString() + " " + newID + " to "
                            + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod
                            + " with integer " + newValue);
                }
                //If the base and new list contain it, and the values are different, it re-adds the value.
            } else if (!baselist.sfi.get(i).equals(newlist.sfi.get(i))) {
                first.add(new SubFormInt(this.counterType, newID, newValue));
                if (SkyBash.fullLogging) {
                    SPGlobal.log(type[0].toString() + ": ", "Merged integer for " + type[0].toString() + " for ID " + newID + " to "
                            + SkyBash.currentRecord + " from Mod " + SkyBash.currentMod
                            + " to integer " + newValue);
                }
            }
        }

        tempList.clear();
        tempList.addRecordsTo(first.sfi);
        return tempList;
    }

    SubList<LVLO> mergeListLVLO(SubList<LVLO> newSubList, SubList<LVLO> baseSubList, MajorRecord m) {
        SubList<LVLO> subList = (SubList<LVLO>) this;

        /*
         * Note to self: possibly use maps? Can rework to use specialized maps,
         * one for each LVLI/LVLN, of the elements and the integer number of
         * each element. Allows for easy checks of existence and counts.
         *
         * Reconsider for another day, or if this proves to NOT work.
         */

        //Directly accessing the collections to clarify code, rework into specialized
        //classes set up of lists of LVLO's w/ numbers of elements.
        LVLOList list = new LVLOList(subList.collection);
        LVLOList newList = new LVLOList(newSubList.collection);
        LVLOList baseList = new LVLOList(baseSubList.collection);

        LVLOList removedEntries;

        //List of entries that have previously been removed from the base list.
        //This is to prevent removal of multiple entries, and to provide
        //consistency over a series of mergers.
        if (SkyBash.leveledRecordMap.containsKey(m.getForm())) {
            removedEntries = SkyBash.leveledRecordMap.get(m.getForm());
        } else {
            removedEntries = new LVLOList();
            SkyBash.leveledRecordMap.put(m.getForm(), removedEntries);
        }

        for (LVLO entry : newList.list.keySet()) {
            LVLO l = entry;
            //If the base list doesn't contain an entry on the new list, add it.
            if (!baseList.contains(l)) {
                for (int i = 0; i < newList.getCount(l); i++) {
                    list.add(l);
                }
            } //If it does contain it, add the difference between them.
            else {
                if (baseList.getCount(l) < newList.getCount(l)) {
                    for (int i = 0; i < (newList.getCount(l) - baseList.getCount(l)); i++) {
                        list.add(l);
                    }
                }
            }
        }
        //Only delete elements off the list w/ the delev tag.
        if (SkyBash.mTags.Delev) {
            //Iterate through the base list, cross-check with the newer list.
            for (LVLO entry : baseList.list.keySet()) {
                LVLO l = entry;
                //If the new list does not contain this record, it needs entirely removed.
                if (!newList.contains(l)) {
                    //If the list of removed entries contains it, it's already been fully or
                    //partially removed.
                    //Also checks that the base num is greater than the current number of
                    //removed entries - if it's not, then more than needed has already been
                    //fully removed.
                    if (removedEntries.contains(l) && baseList.getCount(l) > removedEntries.getCount(l)) {
                        for (int i = 0; i < (baseList.getCount(l) - removedEntries.getCount(l)); i++) {
                            list.remove(l);
                            removedEntries.add(l);
                        }
                    } //If it doesn't contain it, then it needs to be listed in the removed entries
                    //so it's not removed multiple times (or attempted to do that).
                    else {
                        for (int i = 0; i < baseList.getCount(l); i++) {
                            list.remove(l);
                            removedEntries.add(l);
                        }
                    }
                } //If the new list does contain this record, then it only needs partially removed.
                else {
                    //Check that the new list has less than the base (if it has more, there's no need
                    //to remove elements from the list!).
                    if (newList.getCount(l) < baseList.getCount(l)) {
                        //If it has less than the base, check that the difference is greater than
                        //the number of already removed entries.
                        if (removedEntries.contains(l) && (baseList.getCount(l) - newList.getCount(l)) > removedEntries.getCount(l)) {
                            //If the difference is greater, remove the difference and note the additional removal
                            //in the removed entries section.
                            for (int i = 0; i < ((baseList.getCount(l) - newList.getCount(l)) - removedEntries.getCount(l)); i++) {
                                list.remove(l);
                                removedEntries.add(l);
                            }
                        } //If there is no current note of removed entries, just remove the difference
                        //and add that difference to the list of removed entries.
                        else {
                            for (int i = 0; i < (baseList.getCount(l) - newList.getCount(l)); i++) {
                                list.remove(l);
                                removedEntries.add(l);
                            }
                        }
                    }
                }
            }
        }
        return subList;
    }

    /**
     * A specialized class that stores information for the above mergers,
     * involving a sublist of SubFormInts.
     */
    class SubFormIntStorage {

        ArrayList<SubFormInt> sfi = new ArrayList<>();
        ArrayList<FormID> id = new ArrayList<>();
        ArrayList<Integer> i = new ArrayList<>();
        HashMap<FormID, Integer> map = new HashMap<>();
        int size = 0;

        SubFormIntStorage(SubList<SubFormInt> list) {
            for (int j = 0; j < list.size(); j++) {
                this.add(list.get(j));
                map.put(list.get(j).ID, list.get(j).num);
            }

        }

        SubFormIntStorage(SubFormInt sfi) {
            this.sfi.add(sfi);
            this.id.add(sfi.ID);
            this.i.add(sfi.num);
            size = 1;
        }

        private void add(SubFormInt sfi) {
            this.sfi.add(sfi);
            this.id.add(sfi.ID);
            this.i.add(sfi.num);
            size++;
        }

        private void remove(FormID id) {
            int index = this.id.indexOf(id);
            this.sfi.remove(index);
            this.id.remove(index);
            this.i.remove(index);
            size--;
        }
    }

    @Override
    void parseData(LStream in) throws BadRecord, DataFormatException, BadParameter, IOException {
        Type t = getNextType(in);
        if (counterType != t) {
            if (t.equals(type[0])) {
                T newRecord = (T) prototype.getNew(type[0]);
                newRecord.parseData(in);
                add(newRecord);
            } else {
                get(size() - 1).parseData(in);
            }
        }
    }

    @Override
    SubRecord getNew(Type type) {
        throw new UnsupportedOperationException("Not supported.");
    }

    @Override
    int getContentLength(Mod srcMod) {
        int length = 0;
        if (counterType != Type.NULL) {
            length += counterLength + 6;
        }
        for (SubRecord r : collection) {
            length += r.getTotalLength(srcMod);
        }
        return length;
    }

    @Override
    void export(LExporter out, Mod srcMod) throws IOException {
        if (isValid()) {
            if (counterType != Type.NULL) {
                SubData counter = new SubData(counterType, Ln.toByteArray(collection.size(), counterLength));
                counter.export(out, srcMod);
            }
            Iterator<T> iter = iterator();
            while (iter.hasNext()) {
                iter.next().export(out, srcMod);
            }
        }
    }

    static ArrayList<FormID> subFormToPublic(SubList<SubForm> in) {
        ArrayList<FormID> out = new ArrayList<FormID>(in.size());
        for (SubForm s : in) {
            out.add(s.ID);
        }
        return out;
    }

    static ArrayList<SubFormInt> subFormIntToPublic(SubList<SubFormInt> in) {
        ArrayList<SubFormInt> out = new ArrayList<>(in.size());
        for (SubFormInt s : in) {
            out.add(s);
        }
        return out;
    }

    static ArrayList<Integer> subIntToPublic(SubList<SubInt> in) {
        ArrayList<Integer> out = new ArrayList<Integer>(in.size());
        for (SubInt s : in) {
            out.add(s.get());
        }
        return out;
    }

    static ArrayList<String> subStringToPublic(SubList<SubString> in) {
        ArrayList<String> out = new ArrayList<String>(in.size());
        for (SubString s : in) {
            out.add(s.string);
        }
        return out;
    }

    static ArrayList<byte[]> subDataToPublic(SubList<SubData> in) {
        ArrayList<byte[]> out = new ArrayList<byte[]>(in.size());
        for (SubData s : in) {
            out.add(s.data);
        }
        return out;
    }

    ArrayList<T> toPublic() {
        return collection;
    }

    /**
     *
     * @return An iterator of all records in the SubRecordList.
     */
    @Override
    public Iterator<T> iterator() {
        return collection.listIterator();
    }

    @Override
    ArrayList<FormID> allFormIDs() {
        ArrayList<FormID> out = new ArrayList<FormID>();
        for (T item : collection) {
            out.addAll(item.allFormIDs());
        }
        return out;
    }

    @Override
    void fetchStringPointers(Mod srcMod, Record r, Map<SubStringPointer.Files, LChannel> streams) throws IOException {
        for (SubRecord s : collection) {
            s.fetchStringPointers(srcMod, r, streams);
        }
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 89 * hash + Objects.hashCode(this.collection);
        return hash;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null) {
            return false;
        }
        if (!(o instanceof SubList)) {
            return false;
        }
        SubList s = (SubList) o; // Convert the object to a Person
        return (this.collection.equals(s.collection));
    }
}
